package org.chiefmedicalofficer.mcd.model;

import com.hp.hpl.jena.query.QueryExecution;
import com.hp.hpl.jena.query.QueryExecutionFactory;
import com.hp.hpl.jena.query.QuerySolution;
import com.hp.hpl.jena.query.ResultSet;
import com.hp.hpl.jena.rdf.model.InfModel;
import com.hp.hpl.jena.rdf.model.Literal;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.rdf.model.NodeIterator;
import com.hp.hpl.jena.rdf.model.Property;
import com.hp.hpl.jena.rdf.model.RDFNode;
import com.hp.hpl.jena.rdf.model.RDFReader;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.ResourceFactory;
import com.hp.hpl.jena.reasoner.Reasoner;
import com.hp.hpl.jena.reasoner.ReasonerRegistry;
import com.hp.hpl.jena.reasoner.ValidityReport;
import com.hp.hpl.jena.vocabulary.DC;
import com.hp.hpl.jena.vocabulary.RDF;
import com.hp.hpl.jena.vocabulary.RDFS;
import com.hp.hpl.jena.vocabulary.ReasonerVocabulary;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;

/**
 * MCD Model - stores and retrieves information about pages and their 
 * relationships.
 *
 * Generalized semantic web graph store with convenience methods.
 * The public interface is typically String values, RDF/XML, or in one of the
 * predefined entity classes (Page, Predicate, Property). Applications using
 * this public interface need not manipulate the Jena model directly.
 *
 * Pages are the first-class citizen. Each page represents a class of Thing
 * (e.g. home page, department page, service page). It has a list of
 * attributes (Literal triples), properties (object triples, relatonships to
 * other pages), and defined predicates (for which this class is in the domain).
 *
 * Contains two models: assertions (all the data)
 * and vocabulary (for fast lookup of the class and predicate structure)
 *
 * Each model implements a simple RDFS reasoner, just the transitive closure
 * of subPropertyOf and subClassOf relations, the domain and range entailments
 * and the implications of subPropertyOf and subClassOf.
 *
 *
 * @author Tom Wilson, ChiefmedicalOfficer.org twilson650@gmail.com
 *
 * Copyright 2010 Thomas J. Wilson, ChiefMedicalOfficer.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

public class GraphStore {

    private static GraphStore graphStore = null;
    private static InfModel assertions = null;
    private static Reasoner assertionReasoner = null;
    /**
     * implements just the transitive closure of subPropertyOf and
     * subClassOf relations, the domain and range entailments and the
     * implications of subPropertyOf and subClassOf. It omits all of the
     * axioms. This is probably the most useful mode but is not the default
     * because it is a less complete implementation of the standard.
     * The level can be set using the setParameter call.
     */
    private static Reasoner vocabularyReasoner = null;
    private static InfModel vocabulary = null;
    static Logger log = Logger.getLogger(GraphStore.class);

    /**
     * Sets up the two models and corresponding reasoners:
     * one for vocabulary one for assertions.
     */
    protected GraphStore() {
        log.setLevel(Level.DEBUG);

        vocabularyReasoner = ReasonerRegistry.getRDFSReasoner();
        vocabularyReasoner.setParameter(ReasonerVocabulary.PROPsetRDFSLevel,
                ReasonerVocabulary.RDFS_SIMPLE);
        assertionReasoner = ReasonerRegistry.getRDFSReasoner();
        assertionReasoner.setParameter(ReasonerVocabulary.PROPsetRDFSLevel,
                ReasonerVocabulary.RDFS_SIMPLE);

        // read the MCD ontology from one file
        vocabulary = ModelFactory.createInfModel(vocabularyReasoner,
                ModelFactory.createDefaultModel());
        // read the ontology+data into another.
        assertions = ModelFactory.createInfModel(assertionReasoner,
                ModelFactory.createDefaultModel());

    }

    /**
     * Singleton. This class holds the entire graph store, so we only want
     * one copy in memory. It can provide sub-graphs in the form of RDF/XML
     * @return
     */
    public static GraphStore getInstance() {
        if (graphStore == null) {
            graphStore = new GraphStore();
        }
        return graphStore;
    }

    /**
     * Loads a vocabulary from a URL. This should have basic ontology but
     * not entities. Can resolve URL types, e.g. file: vs http: and
     * expects RDF/XML. (Support for other serializations are possible).
     * 
     * This is a small, fast graph holding the class/predicate structure
     * to query the vocabulary. Can be called multiple times to import
     * from different sources.
     *
     * @param ontologyUrl - resolvable URL locating a file with RDF/XML or
     * other supported vocabulary.
     */
    public void loadVocabulary(String ontologyUrl, String base) {
        vocabulary.read(ontologyUrl, base, null);
        assertions.add(vocabulary);
        ValidityReport validity = vocabulary.validate();
        if (validity.isValid()) {
            System.out.println("OK");
        } else {
            System.out.println("Conflicts");
            for (Iterator i = validity.getReports(); i.hasNext();) {
                log.error(" - " + i.next());
            }
        }
        validity = assertions.validate();
        if (validity.isValid()) {
            System.out.println("OK");
        } else {
            System.out.println("Conflicts");
            for (Iterator i = validity.getReports(); i.hasNext();) {
                log.error(" - " + i.next());
            }
        }
    }

    /**
     * Loads the full data graph into memory. Can be called multiple times
     * to add data from different sources.
     * @param modelUrl
     */
    public void loadDataModel(String modelUrl, String base) {
        assertions.read(modelUrl, base, null);
        ValidityReport validity = assertions.validate();
        if (validity.isValid()) {
            System.out.println("OK");
        } else {
            System.out.println("Conflicts");
            for (Iterator i = validity.getReports(); i.hasNext();) {
                log.error(" - " + i.next());
            }
        }
    }

    /**
     * Adds RDF/XML statements to the model.
     * 
     * @param rdf - a String containing RDF statements.
     * @param base - the base uri to be used when converting relative URI's to
     * absolute URI's. (Resolving relative URIs and fragment IDs is done by
     * prepending the base URI to the relative URI/fragment.) If there are
     * no relative URIs in the source, this argument may safely be null.
     * If the base is the empty string, then relative URIs will be retained
     * in the model. This is typically unwise and will usually generate errors
     * when writing the model back out.
     *
     * This is another option for loading RDF/XML into the model.
     */
    public void addStatements(String rdf, String base) {
        RDFReader rdfReader = assertions.getReader();
        rdfReader.read(assertions, new StringReader(rdf), base);
    }

    /**
     * Add a triple where the object is a resource URI.
     * @param subjectUri
     * @param predicateUri
     * @param objectUri
     */
    public void addResourceTriple(String subjectUri,
            String predicateUri, String objectUri) {
        assertions.add(
                assertions.createResource(subjectUri),
                assertions.createProperty(predicateUri),
                assertions.createResource(objectUri));
    }

    /**
     * Remove a triple where the object is a resource URI.
     * @param subjectUri
     * @param predicateUri
     * @param objectUri
     */
    public void removeResourceTriple(String subjectUri,
            String predicateUri, String objectUri) {
        assertions.remove(
                assertions.createResource(subjectUri),
                assertions.createProperty(predicateUri),
                assertions.createResource(objectUri));
    }

    /**
     * Add a triple where the object is a literal value.
     * Currently only String datatype is supported.
     * @param subjectUri
     * @param predicateUri
     * @param objectLiteral
     */
    public void addLiteralTriple(String subjectUri,
            String predicateUri, String objectLiteral) {
        assertions.addLiteral(
                assertions.createResource(subjectUri),
                assertions.createProperty(predicateUri),
                assertions.createLiteral(objectLiteral));
    }

    /**
     * Remove a triple where the object is a literal value.
     * @param subjectUri
     * @param predicateUri
     * @param objectLiteral
     */
    public void removeLiteralTriple(String subjectUri,
            String predicateUri, String objectLiteral) {
        assertions.remove(
                assertions.createResource(subjectUri),
                assertions.createProperty(predicateUri),
                assertions.createLiteral(objectLiteral));
    }

    /**
     * An attribute is a literal triple and functional property,
     * i.e. a given entity can only have one instance of this property.
     *
     * @param subjectUri
     * @param predicateUri
     * @param attribute
     */
    public void setAttribute(String subjectUri,
            String predicateUri, String attribute) {
        NodeIterator ni = assertions.listObjectsOfProperty(
                ResourceFactory.createResource(subjectUri),
                ResourceFactory.createProperty(predicateUri));
        while (ni.hasNext()) {
            RDFNode n = ni.next();
            if (n.isLiteral()) {
                assertions.remove(assertions.createStatement(
                        assertions.createResource(subjectUri),
                        assertions.createProperty(predicateUri),
                        (Literal) n));
            }
        }
        assertions.addLiteral(
                assertions.createResource(subjectUri),
                assertions.createProperty(predicateUri),
                assertions.createLiteral(attribute));
    }

    /**
     * Get an attribute, i.e. functional literal value.
     * @param subjectUri
     * @param predicateUri
     * @return
     */
    public String getAttribute(String subjectUri, String predicateUri) {
        String attributeValue = null;
        NodeIterator ni = assertions.listObjectsOfProperty(
                ResourceFactory.createResource(subjectUri),
                ResourceFactory.createProperty(predicateUri));
        while (ni.hasNext()) {
            RDFNode n = ni.next();
            if (n.isLiteral()) {
                Literal attributeLiteral = (Literal) n;
                attributeValue = attributeLiteral.getString();
                break;
            }
        }
        return attributeValue;
    }
    
    /**
     * Returns a String with the entire data model in RDF/XML.
     * @return
     */
    public String getDataModel() {
        StringWriter rdfWriter = new StringWriter();
        assertions.write(rdfWriter);
        return rdfWriter.toString();
    }

    /**
     * First determines if the object's class can be inferred from the assertions.
     * 
     * If not, it checks to see if the class can be found with the query
     * "?class <hasUriPattern> <entityUri>"
     *
     * The optional hasUriPattern attribute can be used to define where in a
     * web site where a class can be found. Given the messy state of real-world
     * data, I'm questioning the usefulness of inference by URI pattern. 
     * Generally it is better for the calling program to determine the
     * class the first time an entity is used.
     *
     * The class URI is used to obtain a list of predicates applicable to
     * that class when adding new object properties.
     *
     * @param entityUri
     * @return the most specific subclass, i.e. the class for which does not
     * have any subclasses that also define this object.
     */
    public List<String> inferClass(String entityUri) {
        boolean foundClass = false;
        String queryString = "SELECT ?c \n" +
                "WHERE {<" + entityUri + "> " +
                "<" + RDF.type.getURI() + "> ?c  . \n" +
                "OPTIONAL {?sc <" + RDFS.subClassOf + "> ?c . \n" +
                "<" + entityUri + "> <" + RDF.type.getURI() + "> ?sc . " +
                "FILTER (?sc != ?c) } . \n" +
                "FILTER(!bound(?sc)) }";
        System.out.println(queryString);
        QueryExecution ex = QueryExecutionFactory.create(queryString, assertions);
        ResultSet rs = ex.execSelect();
        List<String> result = new ArrayList<String>();
        while (rs.hasNext()) {
            QuerySolution qs = rs.next();
            RDFNode c = qs.get("c");
            if (c.isResource()) {
                Resource cR = (Resource) c;
                result.add(cR.getURI());
                foundClass = true;
            }
        }
        if (foundClass) {
            return result;
        } else {
            // TODO: look for URI pattern

            return null;
        }
    }

    /**
     * For a given class, return a list of all applicable predicates,
     * as defined by existence in the predicate domain(s).
     * 
     * This list represents the properties one can assign for a given class.
     *
     * For each property, also returns the property label and all subProperty
     * relationships. Returns the label, intended to be used as a predicate in
     * a natural-sounding triple (e.g. "has published").
     * @param subjectClass
     * @return String containing RDF/XML statements: predicates, labels, and
     * subProperty relationships.
     */
    public List<PredicateDescription> getPredicates(String classUri) {
        if (classUri == null || classUri.trim().length() == 0) {
            return null;
        }
        List<PredicateDescription> predicates = new ArrayList<PredicateDescription>();
        String queryString = SparqlGenerator.getPredicates(classUri);
        System.out.println("queryString = " + queryString);
        QueryExecution ex = QueryExecutionFactory.create(queryString, assertions);
        ResultSet rs = ex.execSelect();
        while (rs.hasNext()) {
            String uri = null, label = null, parent = null;
            QuerySolution qs = rs.next();
            Resource uriResource = (Resource) qs.getResource("prop");
            if (uriResource != null) {
                uri = uriResource.getURI();
            }
            Literal labelLiteral = (Literal) qs.getLiteral("label");
            if (labelLiteral != null) {
                label = labelLiteral.getString();
            }
            Resource parentResource = (Resource) qs.getResource("parent");
            if (parentResource != null) {
                parent = parentResource.getURI();
            }
            if (uri == null || uri.equals(parent)) {
                // a property can be its own sub-property
                continue;
            }
            PredicateDescription predicate = new PredicateDescription();
            predicate.setUri(uri);
            predicate.setLabel(label);
            predicate.setParent(parent);
            predicates.add(predicate);
        }
        return predicates;
    }

    /**
     * Returns a list of properties for a given entity, as defined by
     * all tirples in which the entity URI is the subject.
     * Builds Property and Predicate entity objects, including attributes:
     * labels and titles for the properties and their predicates.
     * @param subjectUrl
     * @return a list of predicate entity objects
     */
    public List<PropertyDescription> getObjectProperties(String subjectUri) {

        List<PropertyDescription> properties =
                new ArrayList<PropertyDescription>();

        String queryString = SparqlGenerator.getObjectProperties(subjectUri);
        log.debug("query string = " + queryString);
        QueryExecution ex = QueryExecutionFactory.create(queryString, assertions);
        ResultSet rs = ex.execSelect();
        while (rs.hasNext()) {
            String pred = null, predTitle = null, predLabel = null,
                    parentPred = null, obj = null, objTitle = null;
            QuerySolution qs = rs.next();
            RDFNode objNode = qs.get("obj");
            if (objNode.isResource()) {
                Resource objResource = qs.getResource("obj");
                if (objResource != null) {
                    obj = objResource.getURI();
                }
            } else if (objNode.isLiteral()) {
                // Do not add literals
//                Literal objLiteral = qs.getLiteral("obj");
//                obj = objLiteral.getString();
                continue;
            } else {
                // only process object properties (resources)
                continue;
            }
            Literal objTitleLiteral = qs.getLiteral("objTitle");
            if (objTitleLiteral != null) {
                objTitle = objTitleLiteral.getString();
            }
            Resource predResource = qs.getResource("pred");
            if (predResource != null) {
                pred = predResource.getURI();
            }
            Literal predTitleLiteral = qs.getLiteral("predTitle");
            if (predTitleLiteral != null) {
                predTitle = predTitleLiteral.getString();
            }
            Literal predLabelLiteral = qs.getLiteral("predLabel");
            if (predLabelLiteral != null) {
                predLabel = predLabelLiteral.getString();
            }
            Resource parentPredResource = qs.getResource("parentPred");
            if (parentPredResource != null) {
                parentPred = predResource.getURI();
            }

            PredicateDescription predicate = new PredicateDescription();
            predicate.setUri(pred);
            predicate.setLabel(predLabel);
            predicate.setTitle(predTitle);
            predicate.setParent(parentPred);

            PropertyDescription property = new PropertyDescription();
            property.setSubjectUri(subjectUri);
            property.setObjectUri(obj);
            property.setObjectTitle(objTitle);
            property.setPredicate(predicate);

            properties.add(property);
        }
        return properties;
    }


    /**
     * Relate two pages in a triple.
     *
     * Assumes the predicate already exists in the model, but will add
     * attributes to the subject and object if necessary.
     *
     * If the subject and/or object pages do not exist in the model,
     * then add label, title, and any other properties to build a description.
     * @param subject
     * @param predicateUri
     * @param objectPage
     */
    public void relatePages(PageDescription subjectPage, String predicateUri,
            PageDescription objectPage) {
        Resource subjectResource = assertions.createResource(subjectPage.getPageUri());
        Property predicateResource = assertions.createProperty(predicateUri);
        Resource objectResource = assertions.createResource(objectPage.getPageUri());
        // add subject properties (title, label, etc)
        if (!assertions.contains(subjectResource, RDFS.label)) {
            assertions.add(subjectResource, RDFS.label, subjectPage.getLabel());
        }
        if (!assertions.contains(subjectResource, DC.title)) {
            assertions.add(subjectResource, DC.title, subjectPage.getTitle());
        }

        // add object properties (title, label, etc)
        if (!assertions.contains(objectResource, RDFS.label)) {
            assertions.add(objectResource, RDFS.label, objectPage.getLabel());
        }
        if (!assertions.contains(objectResource, DC.title)) {
            assertions.add(objectResource, DC.title, objectPage.getTitle());
        }

        // add the relationship
        if (!assertions.contains(subjectResource,
                predicateResource, objectResource)) {
            assertions.add(subjectResource,
                    predicateResource,
                    objectResource);
        }

    }

    /**
     * Execute arbitrary SPARQL with either a DESCRIBE or CONSTRUCT.
     *
     * Does not support SELECT.
     *
     * @param queryString
     * @return a String containing triples in RDF/XML
     */
//    public String adHocQuery(String queryString) {
//        // Create a new query passing a String containing the RDQL to execute
//        QueryExecution ex = QueryExecutionFactory.create(queryString, assertions);
//        Model resultModel = null;
//        if (queryString.startsWith("CONSTRUCT")) {
//            resultModel = ex.execConstruct();
//        } else if (queryString.trim().startsWith("DESCRIBE")) {
//            resultModel = ex.execDescribe();
//        }
//
//        if (resultModel != null) {
//            StringWriter rdfWriter = new StringWriter();
//            resultModel.write(rdfWriter);
//            resultModel.close();
//            return rdfWriter.toString();
//        } else {
//            return null;
//        }
//    }

    /**
     * Produces a block of RDF/XML containing properties and attributes
     * for a given entity, as implemented by the SPARQL DESCRIBE query.
     * @param subjectUri
     * @return
     */
    public String describeRDF(String subjectUri) {
        String queryString = "DESCRIBE <" + subjectUri + "> ?pred ?obj "
                + "WHERE {<" + subjectUri + "> ?pred ?obj}";
        log.debug("query string = " + queryString);
        QueryExecution ex = QueryExecutionFactory.create(queryString, assertions);
        Model resultModel = null;
        resultModel = ex.execDescribe();
        if (resultModel != null) {
            StringWriter rdfWriter = new StringWriter();
            resultModel.write(rdfWriter);
            resultModel.close();
            return rdfWriter.toString();
        } else {
            return null;
        }
    }

    /**
     * Close all models.
     */
    public void destroy() {
        vocabulary.close();
        assertions.close();
        graphStore = null;
    }

    /**
     * Erase all vocabulary and assertions.
     */
    public void erase() {
        vocabulary.removeAll();
        assertions.removeAll();
    }
}
